AWS CDK Goを使ってTransit GatewayでVPC間を接続してみた
こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。
みなさんGo言語好きですか?
AWS CDKではGo言語で構築可能ですが、検索するとTypeScriptの記事が多いですよね?
最近私はAWS CDKのGo言語を使ってシンプルにEC2を構築したりしています。
今回作成する構成は以下のようなシンプルな構成です。
ゴールは以下のようにHub VPCのEC2インスタンスからSpoke VPCのEC2インスタンスにpingで疎通確認をすることです。
なお、今回作成したコードは細かくファイルを分けており、すべてを紹介する訳ではありません。
全体を確認したい場合は以下のリポジトリをご確認ください。
記述したコード
重要そうなポイントを抜粋しつつ、コードを紹介します。
ディレクトリ構成は以下のようになっています。
. ├── README.md ├── cdk.json ├── cmd │ ├── hub_spoke #Transit Gateway周りのリソース群を作成 │ │ ├── hub.go │ │ ├── main.go │ │ ├── route_establish .go │ │ └── route_table.go │ ├── network # VPCやルートテーブルなどのリソース群を作成 │ │ ├── route_table.go │ │ └── vpc.go │ └── server # EC2周りのリソース群を作成 │ └── main.go ├── go.mod ├── go.sum ├── transit_gw.go #今回のmainファイルに該当 └── transit_gw_test.go
VPC・EC2関連リソースの作成
VPC・VPCエンドポイントなどのリソースについて、構造体とメソッドを用意して2セット作成しています。
VPC関連リソースを作成するコード
以下のように構造体とメソッドを準備しています。
type Network struct { scope constructs.Construct vpcName string cidr string hasSSMEndpoint bool } func (nr Network) CreateNetworkResources() awsec2.Vpc { // VPC vpc := awsec2.NewVpc(nr.scope, &nr.vpcName, &awsec2.VpcProps{ IpAddresses: awsec2.IpAddresses_Cidr(jsii.String(nr.cidr)), MaxAzs: jsii.Number(2), EnableDnsSupport: jsii.Bool(true), EnableDnsHostnames: jsii.Bool(true), VpcName: jsii.String(nr.vpcName), SubnetConfiguration: &[]*awsec2.SubnetConfiguration{ { Name: jsii.String("TransitGateway"), SubnetType: awsec2.SubnetType_PRIVATE_ISOLATED, // Transit Gatewayのアタッチメントを配置するサブネットのためCidrMaskは小さくしている CidrMask: jsii.Number(28), }, { Name: jsii.String("Private"), SubnetType: awsec2.SubnetType_PRIVATE_ISOLATED, CidrMask: jsii.Number(24), }, }, }) // 指定した時のみVPCエンドポイントを追加 if nr.hasSSMEndpoint { vpc.AddInterfaceEndpoint(jsii.String("SSM"), &awsec2.InterfaceVpcEndpointOptions{ Service: awsec2.InterfaceVpcEndpointAwsService_SSM(), }) vpc.AddInterfaceEndpoint(jsii.String("SSMMessage"), &awsec2.InterfaceVpcEndpointOptions{ Service: awsec2.InterfaceVpcEndpointAwsService_SSM_MESSAGES(), }) vpc.AddInterfaceEndpoint(jsii.String("EC2Messag"), &awsec2.InterfaceVpcEndpointOptions{ Service: awsec2.InterfaceVpcEndpointAwsService_EC2_MESSAGES(), }) } return vpc }
これをmain処理から以下のように2セット作成しています。(HubVPCとSpokeVPC分)
// Hub(Shared) VPCに該当 sharedNetworkResource := network.NewNetwork(stack, "SharedVpc", "10.10.0.0/16", true) sharedVpc := sharedNetworkResource.CreateNetworkResources() severResource := server.NewServer(stack, "SharedVPCInstance", sharedVpc) severResource.CreateServerResources() // SpokeVPCに該当 workloadNetwork := network.NewNetwork(stack, "WorkloadVpc", "10.20.0.0/16", false) workloadVpc := workloadNetwork.CreateNetworkResources() workloadServer := server.NewServer(stack, "WorkloadVPCInstance", workloadVpc) workloadServer.CreateServerResources()
次にEC2の作成部分は以下のようになっています。
EC2を作成
type Server struct { scope constructs.Construct name string vpc awsec2.Vpc } func (sr Server) CreateServerResources() { sg := awsec2.NewSecurityGroup(sr.scope, jsii.String(sr.name+"SG"), &awsec2.SecurityGroupProps{ AllowAllOutbound: jsii.Bool(true), Vpc: sr.vpc, }) // allow sg to inbound icmp sg.AddIngressRule(awsec2.Peer_Ipv4(jsii.String("10.0.0.0/8")), awsec2.Port_AllIcmp(), jsii.String("allow icmp"), nil) awsec2.NewInstance(sr.scope, jsii.String(sr.name), &awsec2.InstanceProps{ InstanceType: awsec2.InstanceType_Of(awsec2.InstanceClass_T3, awsec2.InstanceSize_MICRO), MachineImage: awsec2.MachineImage_LatestAmazonLinux(&awsec2.AmazonLinuxImageProps{ Generation: awsec2.AmazonLinuxGeneration_AMAZON_LINUX_2, }), SsmSessionPermissions: jsii.Bool(true), Vpc: sr.vpc, SecurityGroup: sg, VpcSubnets: &awsec2.SubnetSelection{ SubnetGroupName: jsii.String("Private"), }, }) }
こちらもmain処理から各VPC内のサブネットに作成できるように呼び出します。
// HubVPCに配置するEC2 severResource := server.NewServer(stack, "SharedVPCInstance", sharedVpc) severResource.CreateServerResources() // SpokeVPC配置するEC2 workloadServer := server.NewServer(stack, "WorkloadVPCInstance", workloadVpc) workloadServer.CreateServerResources()
Transit Gateway周りのリソース群を作成
ここから以下ブログを参考に順にリソースを作成します。
- Transit Gatewayを作成
- Transit Gatewayのアタッチメントを作成(各VPCの専用サブネットを指定)
- Transit Gatewayのルートテーブルを作成
- 2で作ったアタッチメントと3で作ったルートテーブルを紐付け(アソシエーション)
- アタッチメントしたVPCからルートテーブルに経路を伝播(プロパゲーション)
- Transit Gatewayのルートテーブルにルートを追加
1. Transit Gatwayを作成
以下のファイルでTransit Gatewayを作成するための構造体とメソッドを用意します。
type Hub struct { scope constructs.Construct } func (h Hub) CreateTransitGateway() awsec2.CfnTransitGateway { return awsec2.NewCfnTransitGateway(h.scope, jsii.String("TransitGateway"), &awsec2.CfnTransitGatewayProps{ DefaultRouteTableAssociation: jsii.String("disable"), DefaultRouteTablePropagation: jsii.String("disable"), }) }
以下のように呼び出します。
hub := NewHub(hp.scope) // Transit Gatewayを作成 tgw := hub.CreateTransitGateway()
次にTransit Gatewayのアタッチメントを各VPCのTransit Gateway専用のサブネットに作成します。
2. Transit Gatewayのアタッチメントを作成(各VPCの専用サブネットを指定)
これまでと同様に構造体とメソッドを用意します。
type VpcAttachment struct { name string vpc awsec2.Vpc tgw awsec2.CfnTransitGateway subnetGroupName string } func (va VpcAttachment) Attach() awsec2.CfnTransitGatewayAttachment { return awsec2.NewCfnTransitGatewayAttachment(va.vpc, jsii.String("VpcAttachment"), &awsec2.CfnTransitGatewayAttachmentProps{ // ↓各VPCのTransit Gateway専用のサブネットを指定 SubnetIds: va.vpc.SelectSubnets(&awsec2.SubnetSelection{SubnetGroupName: &va.subnetGroupName}).SubnetIds, TransitGatewayId: va.tgw.Ref(), VpcId: va.vpc.VpcId(), Tags: &[]*awscdk.CfnTag{ { Key: jsii.String("Name"), Value: jsii.String(va.name), }, }, }) }
以下のように呼び出します。
// Transit GatewayにHub(Shared)VPCをアタッチ attachmentShared := NewVpcAttachment("HubVpcAttachment", hp.sharedVpc, tgw, "TransitGateway") attachmentSharedVpc := attachmentShared.Attach() // Transit GatewayにSpoke VPCをアタッチ attchmentWorkload := NewVpcAttachment("SpokeVpcAttachment", hp.hubVpc, tgw, "TransitGateway") attachmentWorkloadVpc := attchmentWorkload.Attach()
次にTransit Gateway用のルートテーブルを作成します。
3. Transit Gatewayのルートテーブルを作成
Transit Gateway用のルートテーブルを作成する構造体・メソッドを用意します。
type RouteTable struct { name string tgw awsec2.CfnTransitGateway } func (ra RouteTable) Create() awsec2.CfnTransitGatewayRouteTable { return awsec2.NewCfnTransitGatewayRouteTable(ra.tgw, jsii.String("RouteTable"), &awsec2.CfnTransitGatewayRouteTableProps{ TransitGatewayId: ra.tgw.Ref(), Tags: &[]*awscdk.CfnTag{ { Key: jsii.String("Name"), Value: jsii.String(ra.name), }, }, }) }
以下のように呼び出します。
// Transit Gatewayのルートテーブル作成 rt := NewRouteTable("RouteTable", tgw) routeTable := rt.Create()
以下4と5を行います。
4.2で作ったアタッチメントと3で作ったルートテーブルを紐付け(アソシエーション)
5.アタッチメントしたVPCからルートテーブルに経路を伝播(プロパゲーション)
4,5 ルートテーブルへのアタッチメントのアソシエーションとプロパゲーション
構造体とメソッドを以下のように定義します。
type VpcRouteAssociation struct { name string vpcAttachment awsec2.CfnTransitGatewayAttachment routeTable awsec2.CfnTransitGatewayRouteTable } func (vra VpcRouteAssociation) Create() { // Association awsec2.NewCfnTransitGatewayRouteTableAssociation(vra.vpcAttachment, jsii.String(vra.name+"Association"), &awsec2.CfnTransitGatewayRouteTableAssociationProps{ TransitGatewayAttachmentId: vra.vpcAttachment.Ref(), TransitGatewayRouteTableId: vra.routeTable.Ref(), }) // Propagation awsec2.NewCfnTransitGatewayRouteTablePropagation(vra.vpcAttachment, jsii.String(vra.name+"Propagation"), &awsec2.CfnTransitGatewayRouteTablePropagationProps{ TransitGatewayAttachmentId: vra.vpcAttachment.Ref(), TransitGatewayRouteTableId: vra.routeTable.Ref(), }) }
これまでと同様に呼び出します。
// Hub(Shared)VPCのアタッチメントとルートをアソシエーション・プロパゲーション hubVpcRouteAssociation := NewVpcRouteAssociation("HubVpcAssocation", attachmentSharedVpc, routeTable) hubVpcRouteAssociation.Create() // Spoke VPCのアタッチメントとルートをアソシエーション・プロパゲーション spokeVpcRouteAssociation := NewVpcRouteAssociation("SpokeVpcAssociation", attachmentWorkloadVpc, routeTable) spokeVpcRouteAssociation.Create()
次にHub(Shared)VPCとSpoke VPCが相互に通信できるようにTransit Gatewayのルートテーブルにルートを追加します。
6. Transit Gatewayのルートテーブルにルートを追加
type VpcsConnection struct { hubVpc awsec2.Vpc hubVpcAttachment awsec2.CfnTransitGatewayAttachment spokeVpc awsec2.Vpc spokeVpcAttachment awsec2.CfnTransitGatewayAttachment routetable awsec2.CfnTransitGatewayRouteTable } func (cv VpcsConnection) Create() { awsec2.NewCfnTransitGatewayRoute(cv.spokeVpc, jsii.String("ToSpokeVpc"), &awsec2.CfnTransitGatewayRouteProps{ DestinationCidrBlock: cv.spokeVpc.VpcCidrBlock(), TransitGatewayAttachmentId: cv.spokeVpcAttachment.Ref(), TransitGatewayRouteTableId: cv.routetable.Ref(), }) awsec2.NewCfnTransitGatewayRoute(cv.hubVpc, jsii.String("ToHubVpc"), &awsec2.CfnTransitGatewayRouteProps{ DestinationCidrBlock: cv.hubVpc.VpcCidrBlock(), TransitGatewayAttachmentId: cv.hubVpcAttachment.Ref(), TransitGatewayRouteTableId: cv.routetable.Ref(), }) }
長くなりましたがこれでTransit Gateway周りのリソースを作成できました。
各サブネットのルートテーブルにTransit Gatewayへのルートを追加
最後に各サブネットのルートテーブルにTransit Gatewayへのルートを追加します。
サブネットと紐づいているルートテーブルへルートを追加
各VPCのEC2を配置しているサブネットからTransit Gatewayにたどり着けるようにルートを追加します。
type routeToTransitGateway struct { scope constructs.Construct name string vpc awsec2.Vpc tgw awsec2.CfnTransitGateway tgwAttachment awsec2.CfnTransitGatewayAttachment } func (rttg routeToTransitGateway) CreateRouteToTransitGateway() { subnets := rttg.vpc.SelectSubnets(&awsec2.SubnetSelection{ SubnetGroupName: jsii.String("Private"), }).Subnets for i, subnet := range *subnets { routeName := fmt.Sprintf("%s%d", rttg.name, i) awsec2.NewCfnRoute(rttg.scope, jsii.String(routeName), &awsec2.CfnRouteProps{ RouteTableId: subnet.RouteTable().RouteTableId(), DestinationCidrBlock: jsii.String("0.0.0.0/0"), TransitGatewayId: rttg.tgw.Ref(), }).AddDependency(rttg.tgwAttachment) } }
上記の構造体とメソッドを呼び出します。
// EC2が属するサブネットのルートテーブルからTransit Gatewayへのルートを追加 routeHubSubnetToTransit := network.NewRouteToTransitGateway(stack, "HubSubnetToTransitGW", sharedVpc, hubResult.Tgw, hubResult.HubAttachment) routeHubSubnetToTransit.CreateRouteToTransitGateway() routeSpokeSubnetToTransit := network.NewRouteToTransitGateway(stack, "SpokeSubnetToTransitGW", workloadVpc, hubResult.Tgw, hubResult.SpokeAttachment) routeSpokeSubnetToTransit.CreateRouteToTransitGateway()
これにより以下の図のような構成ができました。
EC2からEC2にpingしてみる
最後に以下のようにpingマンドによりVPCを跨いだEC2間で疎通確認をしてみます。
今回はそれぞれのVPCにインターネットへの経路はありませんが、Session ManagerによるEC2への接続をできるようにしている(Hub VPCのみ)ため、接続してpingコマンドを実行します。
ping 10.20.1.209
以下のように無事疎通を確認できました!
64 bytes from 10.20.1.209: icmp_seq=1 ttl=254 time=0.879 ms 64 bytes from 10.20.1.209: icmp_seq=2 ttl=254 time=0.718 ms 64 bytes from 10.20.1.209: icmp_seq=3 ttl=254 time=0.603 ms 64 bytes from 10.20.1.209: icmp_seq=4 ttl=254 time=0.551 ms 64 bytes from 10.20.1.209: icmp_seq=5 ttl=254 time=0.564 ms 64 bytes from 10.20.1.209: icmp_seq=6 ttl=254 time=0.603 ms 64 bytes from 10.20.1.209: icmp_seq=7 ttl=254 time=0.552 ms
最後に: 参考ブログまとめ
今回構築するにあたって以下の記事を参考にしました。
どれも良い記事なのでTransit Gatewayとは?なぜEC2を配置するサブネットとTransit Gatewayのアタッチメントを配置するサブネットを分けているの?と思った方はご参照ください。
- Transit Gatewayを利用してVPC間で通信してみた | DevelopersIO
- この記事を参考に必要な手順を整理しました
- Transit Gatewayでインスペクション用VPCに通信を集約する場合はTransit Gatewayのアプライアンスモードを有効にしよう | DevelopersIO
- この記事はTypeScriptでTransit Gatewayなどを構築していたため、参考にしながらGoに置き換えました
- [AWS Black Belt Online Seminar] AWS Transit Gateway 資料及び QA 公開 | Amazon Web Services ブログ
- わかりやすいサービスの説明とQAで概念再整理に使わせていただきました
次回は異なるVPC間での名前解決の共有について引き続きAWS CDK With Go言語で構成を作って見ようと思います。
このブログが誰かの時間を1秒でも削ればうれしいです。
以上今泉でした。